---
title: Scroll Views
---

Making content scroll on Web vs. Native is a bit different.

If you come from a Web background, you're used to the window automatically scrolling when content grows beyond the height of the page.

On Native, you have to implement a `ScrollView`:

```tsx
import { ScrollView } from 'react-native'

const Scrollable = () => (
  <ScrollView>
    <ContentThatShouldScroll />
  </ScrollView>
)
```

And there you go! Right?

...but if that's all, why is there a whole page dedicated to this?

`ScrollView` has a few quirks on Web in my experience. This page aims to clarify these quirks and show you how to handle them elegantly.

On Web, we occasionally need some tricks for a ScrollView to work as expected, such as adding `height: 100%` to the HTML `body`. The Solito starter handles all that for you with `@expo/next-adapter`, so this isn't something you have to worry about.

However, while the boilerplate is done for you, there are some considerations you need to keep in mind to make `ScrollView` behave the way you want on Web.

We need to solve for a few small issues:

1. **Parent height**: `ScrollView`'s parent needs a fixed height
2. **Window scrolling**: `ScrollView` replaces window scrolling on Web. This means that a mobile browser's URL bar will not collapse as you scroll.

## `ScrollView`'s parent

In order to scroll, `ScrollView` uses the `overflow` CSS property on Web. However, this means that it needs to be taller than its parent to be "overflowing".

If you want a `ScrollView` to handle the scroll, wrap it with a `View` that has `flex: 1`:

```tsx
import { View, ScrollView } from 'react-native'

export default function ArtistPage() {
  return (
    <View style={{ flexGrow: 1, flexBasis: 0 }}>
      <ScrollView>
        <ContentThatShouldScroll />
      </ScrollView>
    </View>
  )
}
```

By adding `flexGrow: 1, flexBasis: 0`, the parent view is fixed to the height of the screen (or its parent).

### Absolute Fill

There may be some situations where `flexGrow: 1, flexBasis: 0` on the parent doesn't work perfectly. In my experience, a fool-proof way to solve it is to absolutely position the parent view:

```tsx
import { View, ScrollView, StyleSheet } from 'react-native'

export default function ArtistPage() {
  return (
    <View style={StyleSheet.absoluteFill}>
      <ScrollView>
        <ContentThatShouldScroll />
      </ScrollView>
    </View>
  )
}
```

This ensures the parent view is the size of its parent, and the `ScrollView` will scroll within it. However, I consider it an escape hatch.

### When not to use Absolute Fill

`StyleSheet.absoluteFill` does not play nicely on Native with screens that have a fixed footer at the bottom, and a keyboard that needs to open. For example, a chat screen with an input at the bottom that sits above the keyboard won't respond correctly to `StyleSheet.absoluteFill`.

If you're building a screen that has an input or some other element that should be fixed to the bottom of the screen, you should stick to `flex: 1` for the parent view.

```tsx
import { View, ScrollView, StyleSheet } from 'react-native'

export default function ArtistPage() {
  return (
    <View style={{ flexGrow: 1, flexBasis: 0 }}>
      {/* Make the messages span full height */}
      <View style={{ flexGrow: 1, flexBasis: 0 }}>
        <ScrollView>
          <MessagesList />
        </ScrollView>
      </View>

      <Composer />
    </View>
  )
}
```

Alternatively, you could apply the absolute position on Web only:

```tsx
import { View, ScrollView, StyleSheet, Platform } from 'react-native'

export default function ArtistPage() {
  return (
    <View
      style={Platform.select({
        web: StyleSheet.absoluteFill,
        default: { flexGrow: 1, flexBasis: 0 },
      })}
    >
      {/* Make the messages span full height */}
      <View style={{ flexGrow: 1, flexBasis: 0 }}>
        <ScrollView>
          <MessagesList />
        </ScrollView>
      </View>

      <Composer />
    </View>
  )
}
```

You'll have to play with it in your own app a little.

## Window scrolling

The solutions above are fine for many cases. However, they all disable window scrolling. On a desktop browser, this doesn't really matter.

But on a mobile browser, opting out of window scrolling affects the UI.

To start, let's compare what it looks like with vs without window scrolling.

| With window scrolling                                                                                                                                                   | Without window scrolling                                                                                                                                                |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <video style={{ maxHeight: '640px'}} controls="controls" src="https://user-images.githubusercontent.com/13172299/166524927-dc65e795-173e-48e2-b8bf-4ccf18c935bf.mov" /> | <video style={{ maxHeight: '640px'}} controls="controls" src="https://user-images.githubusercontent.com/13172299/166524954-7ba65b5a-c4c7-4817-aae2-3689c24ba5a6.MP4" /> |

Watch what happens to the URL bar at the bottom. Notice that, only in the first video, it collapses. This is because it's using window scrolling.

But wait, I thought `ScrollView` got rid of window scrolling. How do we achieve it anyway?

I use a component called `ScreenScrollView`. It lives at the root of basically all scrollable screens.

I use it like this:

```tsx
export default function ArtistPage() {
  return (
    <ScreenScrollView useWindowScrolling={true}>
      <ArtistContent />
    </ScreenScrollView>
  )
}
```

Notice that there is a `useWindowScrolling` prop. This determines if we want to opt-in to window scrolling.

The component code looks something like this:

```tsx
import { View, ScrollView, Platform } from 'react-native'

type Props = React.ComponentProps<typeof ScrollView> & {
  useWindowScrolling?: boolean
}

export function ScreenScrollView({
  useWindowScrolling = true, // defaults to true
  ...props
}: Props) {
  const Component = Platform.select({
    web: useWindowScrolling ? (View as typeof ScrollView) : ScrollView,
    default: ScrollView,
  })

  return <Component {...props} />
}
```

:::tip

Components like `ScreenScrollView` are a good pattern to follow in general. Create your own low-level UI primitives that you use often so that you can update all of your screens in one place.

:::

For screens that should use window scrolling, we use a regular old `View` instead of `ScrollView`, **but only on Web**.

If you're using `useWindowScrolling={true}`, don't wrap `ScreenScrollView` in a `View` with `flex: 1`.

## FlatLists

If you're using FlatList, you probably can't use window scrolling, so you should wrap it with a `View` with `flexGrow: 1, flexBasis: 0`. And if that doesn't work, try the absolute fill escape hatch.

It's worth noting that `FlatList` has zero virtualization on Web, so it doesn't really offer any performance benefit to use it. But it does let you share code. The video shown above of the [BeatGig search](https://beatgig.com/search) uses FlatList.
